namespace BM.Tools.WebControls.MForm.Controls
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;    
    using System.Globalization;
    using System.Linq;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Xml;
    using System.Xml.XPath;

    using BM.Tools.WebControls.MForm.Controls.Helper;
    using BM.Tools.WebControls.MForm.Controls.Items;
    using BM.Tools.WebControls.MForm.Generator;
    using BM.Tools.Xml.Schema.Utils;

    /// <summary>
    /// <para>
    /// Offspring is a repeatable web control that corresponds to some part of the xml data definition. 
    /// </para><para>
    /// By itself, offspring does not have any visual representation in the created HTML code. 
    /// Instead, a number of <see cref="OffspringItem" /> controls is rendered, of which each corresponds to some xml data.
    /// The <see cref="OffspringItem" /> controls are child controls of the Offspring. 
    /// The number of <see cref="OffspringItem" /> controls cannot be less than <see cref="MinOccurs" /> value and 
    /// cannot be more than <see cref="MaxOccurs" /> value.     
    /// </para><para>
    /// The exact number of <see cref="OffspringItem" /> depends on the data that is in the DataSource property, or the data that comes from postback.
    /// The DataSource property is an <see cref="IXPathNavigable" /> in which nodes are found that would match the <see cref="XPathString" /> selector.
    /// </para>
    /// </summary>
    public abstract class Offspring : BaseDataBoundControl, IDataItemContainer, ICompositeControlDesignerAccessor
    {        
        private readonly List<OffspringItem> items = new List<OffspringItem>();

        private AddButton addButton;

        private string displayName;

        private HtmlTextWriterTag itemsTagKey = HtmlTextWriterTag.Div;

        private int maxOccurs = 1;

        private int minOccurs = 1;        

        private string name;

        private NameResolver nameResolver;

        private OffspringsOwner parentBasis;

        private OffspringsOwnerItem parentItem;

        private Root root;
        private PrototypeDataHelper prototypeDataHelper;

        /// <summary>
        /// Initializes a new instance of the <see cref="Offspring"/> class. 
        /// </summary>
        protected Offspring()
        {
            Marker = new Marker(this);
            HiddenInput = new HiddenInput(this);
        }

        /// <summary>
        /// Occurs when an item is created in the <see cref="Offspring" /> control.
        /// </summary>
        public event EventHandler<OffspringItemEventArgs> ItemCreated;

        // ReSharper disable RedundantOverridenMember

        /// <summary>
        /// Gets or sets ID.
        /// </summary>
        public override string ID
        {
            get
            {
                return base.ID;
            }

            set
            {
                base.ID = value;
            }
        }

        // ReSharper restore RedundantOverridenMember

        /// <summary>
        /// Gets AddButton.
        /// </summary>        
        public AddButton AddButton
        {
            get
            {
                if (addButton == null)
                {
                    addButton = new AddButton(this);
                }

                return addButton;
            }
        }

        /// <summary>
        /// Gets HiddenInput.
        /// </summary>
        public HiddenInput HiddenInput
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets a marker control that is inserted so that when more items can be added, it is known where they can be placed.
        /// </summary>        
        public Marker Marker
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets or sets Additions.
        /// </summary>        
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [NotifyParentProperty(true)]
        public ITemplate Additions
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets CommonFooterTemplate.
        /// </summary>        
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [NotifyParentProperty(true)]
        public ITemplate CommonFooterTemplate
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets CommonHeaderTemplate.
        /// </summary>        
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [NotifyParentProperty(true)]
        public ITemplate CommonHeaderTemplate
        {
            get;
            set;
        }

        /// <summary>
        /// Gets Controls, previously ensuring that they were created.
        /// </summary>        
        public override ControlCollection Controls
        {
            get
            {
                EnsureChildControls();
                return base.Controls;
            }
        }

        /// <summary>
        /// Gets DataItem, which returns a item's nodes iterator.
        /// </summary>
        public object DataItem
        {
            get
            {
                return GetNodeIterator(Navigator);
            }
        }

        /// <summary>
        /// Gets data item index.
        /// </summary>        
        public int DataItemIndex
        {
            get
            {
                return 0;
            }
        }

        /// <summary>
        /// Gets display index of the <see cref="IDataItemContainer" /> interface.
        /// </summary>
        public int DisplayIndex
        {
            get
            {
                return 0;
            }
        }

        /// <summary>
        /// Gets or sets a display name. The display name can be used by a label or a title of the child items.
        /// </summary>
        [Localizable(true)]
        public virtual string DisplayName
        {
            get
            {
                if (displayName == null)
                {
                    displayName = Root == null ? DisplayNameHelper.DefaultDisplayName(this, EventArgs.Empty) : Root.DisplayNameMethod(this, EventArgs.Empty);
                }

                return displayName;
            }

            set
            {
                displayName = value;
            }
        }

        /// <summary>
        /// Gets all items, which were created from this control.
        /// </summary>
        public ReadOnlyCollection<OffspringItem> Items
        {
            get
            {
                EnsureChildControls();
                return new ReadOnlyCollection<OffspringItem>(items);
            }
        }

        /// <summary>
        /// Gets or sets the tag key, that will be used in child items. 
        /// </summary>
        [DefaultValue(HtmlTextWriterTag.Div)]
        public HtmlTextWriterTag ItemsTagKey
        {
            get
            {
                return itemsTagKey;
            }

            set
            {
                itemsTagKey = value;
            }
        }

        /// <summary>
        /// Gets local name.
        /// </summary>
        public string LocalName
        {
            get
            {
                return NameResolver.LocalName;
            }
        }

        /// <summary>
        /// Gets or sets maximal number of occurrences.
        /// </summary>        
        [DefaultValue(1)]
        public virtual int MaxOccurs
        {
            get
            {
                return maxOccurs;
            }

            set
            {
                if (value > 0)
                {
                    if (value < MinOccurs)
                    {
                        MinOccurs = value;
                    }

                    maxOccurs = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets minimal number of occurrences.
        /// </summary>        
        [DefaultValue(1)]
        public virtual int MinOccurs
        {
            get
            {
                return minOccurs;
            }

            set
            {
                if (value != minOccurs && value >= 0)
                {
                    if (MaxOccurs < value)
                    {
                        MaxOccurs = value;
                    }
                    
                    minOccurs = value;
                    UpdateNumberOfItems();
                }
            }
        }

        /// <summary>
        /// Gets or sets information how many items should be rendered at least.
        /// Notice that this is not exactly 
        /// </summary>
        public virtual int MinRenderedOccurs
        {
            get
            {
                return ViewState.GetValue("minRenderedOccurs", DefaultMinRenderedOccurs);                
            }

            set
            {
                ViewState["minRenderedOccurs"] = value;
            }
        }

        /// <summary>
        /// Gets or sets the mode of the control
        /// if set, returns its own mode, otherwise returns its parent mode;
        /// </summary>
        [DefaultValue(FormBuilderMode.Edit)]
        public virtual FormBuilderMode Mode
        {
            get
            {
                return ViewState.GetValue("mode", ParentItem == null ? FormBuilderMode.Edit : ParentItem.Mode);                
            }

            set
            {
                ViewState["mode"] = value;
            }
        }

        /// <summary>
        /// Gets or sets the xml qualified name.        
        /// </summary>        
        public string Name
        {
            get
            {
                if (name == null)
                {
                    name = GetNameFromId();
                }

                return name;
            }

            set
            {
                name = value;
                ID = value.Replace(":", "__");
            }
        }

        /// <summary>
        /// Gets name resolver.
        /// </summary>        
        public NameResolver NameResolver
        {
            get
            {
                if (nameResolver == null)
                {
                    XmlNamespaceManager manager = (Root != null) ? Root.Manager : null;
                    nameResolver = new NameResolver(Name, manager);
                }

                return nameResolver;
            }
        }

        /// <summary>
        /// Gets Namespace.
        /// </summary>        
        public string Namespace
        {
            get
            {
                return NameResolver.Namespace;                
            }
        }

        /// <summary>
        /// Gets Navigator.
        /// </summary>        
        public XPathNavigator Navigator
        {
            get
            {
                var dataSource = this.GetDataSource();
                if (dataSource == null)
                {
                    return null;
                }

                return dataSource.CreateNavigator();
            }
        }

        /// <summary>
        /// Gets or sets the ordinal of the current offspring.
        /// </summary>
        [DefaultValue(0)]
        public virtual int Ordinal
        {
            get;
            set;
        }

        /// <summary>
        /// Gets ParentBasis.
        /// </summary>        
        public virtual OffspringsOwner ParentBasis
        {
            get
            {
                if (parentBasis == null)
                {
                    parentBasis = this.ParentOfType<OffspringsOwner>();
                }

                return parentBasis;
            }
        }

        /// <summary>
        /// Gets ParentItem.
        /// </summary>        
        public virtual OffspringsOwnerItem ParentItem
        {
            get
            {
                if (parentItem == null)
                {
                    parentItem = this.ParentOfType<OffspringsOwnerItem>();
                }

                return parentItem;
            }
        }

        /// <summary>
        /// Gets Prefix.
        /// </summary>        
        public string Prefix
        {
            get
            {
                return NameResolver.Prefix;
            }
        }

        /// <summary>
        /// Gets a script component descriptor of a prototype item associated with this offspring.
        /// </summary>
        public ScriptComponentDescriptor PrototypeDescriptor
        {
            get
            {
                return PrototypeDataHelper.PrototypeDescriptor;
            }
        }

        /// <summary>
        /// Gets the root of the offsprings control tree.
        /// </summary>        
        public virtual Root Root
        {
            get
            {
                if (root == null)
                {
                    root = this as Root ?? this.ParentOfType<Root>();
                }

                return root;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether its children will be used in xml.
        /// </summary>
        [DefaultValue(true)]
        public virtual bool XmlEditEnabled
        {
            get
            {
                return ViewState.GetValue("xmlEditEnabled", true);                
            }

            set
            {
                ViewState["xmlEditEnabled"] = value;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the control may have variable number of child items.
        /// </summary>        
        internal virtual bool HasVariableOccurs
        {
            get
            {
                return MinRenderedOccurs != MaxOccurs || DefaultMinRenderedOccurs != MaxOccurs;
            }
        }

        internal virtual string XPathName
        {
            get
            {
                if (Name.Contains(":"))
                {
                    return Name;
                }

                return string.Concat(Root.DefaultPrefix, ":", Name);
            }
        }

        /// <summary>
        /// Gets XPathString.
        /// </summary>        
        internal virtual string XPathString
        {
            get
            {
                var parentChoiceItem = ParentItem as ChoiceItem;
                if (parentChoiceItem != null && !parentChoiceItem.Basis.PassMultipleNodesToChildren)
                {
                    return "self::" + XPathName;
                }

                return XPathName;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the offspring should render a prototype offspring item.
        /// A prototype offspring item is such an offspring item that has Index = 0 and is used as
        /// a prototype template for creating items on the client side.
        /// </summary>
        protected internal bool ShouldRenderPrototype
        {
            get
            {
                if (ParentItem == null)
                {
                    return true;
                }

                return ParentItem.Index == 1 && ParentItem.Basis.ShouldRenderPrototype;
            }
        }

        /// <summary>
        /// Gets information how many items should be rendered at least by default.
        /// This value will be used if the <see cref="MinRenderedOccurs" /> was not set.
        /// </summary>
        protected virtual int DefaultMinRenderedOccurs
        {
            get
            {
                return MinOccurs;
            }
        }

        /// <summary>
        /// Gets the highest index of the child item.
        /// </summary>        
        protected int HighestIndex
        {
            get
            {
                return items.Count == 0 ? 0 : items.Max(item => item.Index);
            }
        }        

        /// <summary>
        /// Gets or sets the highest index of a child item that comes from PostBack.
        /// On client side, the items can be freely added, so the value may change.
        /// </summary>        
        private int PostBackHighestIndex
        {
            get
            {
                int result;
                var isValueValid = int.TryParse(HiddenInput.Value, out result);
                if (isValueValid)
                {
                    return result;
                }

                return ViewState.GetValue("highestIndex", 0);                
            }

            set
            {
                HiddenInput.Value = Convert.ToString(value, CultureInfo.InvariantCulture);
                ViewState["highestIndex"] = value;
            }
        }

        private PrototypeDataHelper PrototypeDataHelper
        {
            get
            {
                if (prototypeDataHelper == null)
                {
                    prototypeDataHelper = new PrototypeDataHelper(this);
                }

                return prototypeDataHelper;
            }
        }

        /// <summary>
        /// creates an item with the next available index
        /// </summary>
        /// <param name="nav">
        /// the navigator that becomes a data source of the created item
        /// </param>        
        /// <returns>
        /// the generated item
        /// </returns>
        public OffspringItem AddItem(XPathNavigator nav)
        {
            return AddItem(nav, HighestIndex + 1);
        }

        /// <summary>
        /// A method that is needed by the <see cref="ICompositeControlDesignerAccessor" />
        /// </summary>
        public void RecreateChildControls()
        {
            ResetChildControls();
            EnsureChildControls();
        }

        /// <summary>
        /// Does not render anything
        /// </summary>
        /// <param name="writer">
        /// The writer.
        /// </param>
        public override void RenderBeginTag(HtmlTextWriter writer)
        {
        }

        /// <summary>
        /// Does not render anything
        /// </summary>
        /// <param name="writer">
        /// The writer.
        /// </param>
        public override void RenderEndTag(HtmlTextWriter writer)
        {
        }

        /// <summary>
        /// resets the mode, so that it will be determined from its parent
        /// </summary>
        public virtual void ResetMode()
        {
            ViewState["mode"] = null;
        }

        /// <summary>
        /// adds item that initially are not to be displayed on the form
        /// but somehow have to be available on the client side 
        /// </summary>
        /// <param name="uniqueId">
        /// The unique Id.
        /// </param>
        /// <param name="clientId">
        /// The client Id.
        /// </param>
        internal void AddPrototypeItem(string uniqueId, string clientId)
        {
            if (HasVariableOccurs)
            {
                var item = CreateItem(0, null);
                item.SetPrototypeId(uniqueId, clientId);
                item.Style.Add("display", "none");
                OnItemCreated(new OffspringItemEventArgs(item, 0));
                Controls.Add(item);
                items.Add(item);                
            }
        }

        /// <summary>
        /// Creates a proper id for the child item.
        /// </summary>
        /// <param name="index">
        /// The index.
        /// </param>
        /// <returns>
        /// the proper child id
        /// </returns>
        internal string CreateClientID(int index)
        {
            return ClientID + "_" + index;
        }

        /// <summary>
        /// creates the item with the given index, adds it to the controls collection
        /// adds it to the Mapping list and invokes the OnItemCreated method
        /// </summary>
        /// <param name="nav">
        /// the navigator that becomes a data source of the created item
        /// </param>
        /// <param name="index">
        /// the index of the item
        /// </param>        
        /// <returns>
        /// the generated item
        /// </returns>
        protected virtual OffspringItem AddItem(XPathNavigator nav, int index)
        {
            var item = CreateItem(index, nav);
            OnItemCreated(new OffspringItemEventArgs(item, index));
            Controls.Add(item);
            items.Add(item);                        
            PostBackHighestIndex = HighestIndex;
            return item;
        }

        /// <summary>
        /// Creates child controls without performing databinding
        /// </summary>
        protected override void CreateChildControls()
        {
            CreateChildControls(false);
        }

        /// <summary>
        /// Creates child controls 
        /// </summary>
        /// <param name="dataBind">
        /// Should perform a databinding on children
        /// </param>
        protected virtual void CreateChildControls(bool dataBind)
        {
            ResetChildControls();
            ChildControlsCreated = true;
            if (CommonHeaderTemplate != null)
            {
                CommonHeaderTemplate.InstantiateIn(this);
            }

            PrototypeDataHelper.CreatePrototypeData();
            Controls.Add(HiddenInput);
            CreateItems(dataBind);
            AddMarker();
            PostBackHighestIndex = HighestIndex;
            if (CommonFooterTemplate != null)
            {
                CommonFooterTemplate.InstantiateIn(this);
            }
        }

        /// <summary>
        /// A method that needs to be implemented by any Offspring heir. 
        /// It should create an <see cref="OffspringItem" /> object of a type appropriate for the class.
        /// </summary>
        /// <param name="index">
        /// The index.
        /// </param>
        /// <param name="navigator">
        /// The navigator.
        /// </param>
        /// <returns>
        /// the offspring item object
        /// </returns>
        protected abstract OffspringItem CreateItem(int index, XPathNavigator navigator);

        /// <summary>
        /// Decides whether to create child items from postback or from datasource
        /// </summary>
        /// <param name="dataBind">
        /// The data bind.
        /// </param>
        protected virtual void CreateItems(bool dataBind)
        {
            if (Root == null)
            {
                return;
            }

            if (DesignMode)
            {
                CreateRemainingItems(0);
                return;
            }

            if (!dataBind)
            {
                CreateItemsFromPostBack();
            }
            else
            {
                CreateItemsFromDataSource();
            }
        }

        /// <summary>
        /// When overridden in a derived class, controls how data is retrieved and bound to the control.
        /// </summary>
        protected override void PerformSelect()
        {
            OnDataBinding(EventArgs.Empty);
            CreateChildControls(true);
            DataBindChildren();            
            OnDataBound(EventArgs.Empty);
        }

        /// <summary>
        /// Creates items based on the datasource attached 
        /// to the control
        /// </summary>
        protected virtual void CreateItemsFromDataSource()
        {
            XPathNodeIterator nodeIterator = GetNodeIterator(Navigator);
            int count = 0;
            if (nodeIterator != null)
            {
                foreach (XPathNavigator nav in nodeIterator)
                {
                    if (count >= MaxOccurs)
                    {
                        break;
                    }

                    AddItem(nav);
                    count++;
                }
            }

            CreateRemainingItems(count);
        }

        /// <summary>
        /// creates items based on the page request
        /// </summary>
        protected virtual void CreateItemsFromPostBack()
        {
            int count = 0;
            for (int i = 1; i <= PostBackHighestIndex; i++)
            {
                var id = CreateClientID(i);
                var isDeleted = Page.Request[id + "_deleted"];
                if (isDeleted != null)
                {
                    continue;
                }

                if (count >= MaxOccurs)
                {
                    break;
                }

                var item = AddItem(null, i);
                item.OnPostBack();
                count++;
            }

            CreateRemainingItems(count);
        }

        /// <summary>
        /// returns a node iterator over nodes from the data source that match the offspring name
        /// </summary>
        /// <param name="navigator">
        /// The navigator.
        /// </param>
        /// <returns>
        /// the iterator over child item nodes.
        /// </returns>
        protected virtual XPathNodeIterator GetNodeIterator(XPathNavigator navigator)
        {
            if (navigator == null)
            {
                return null;
            }

            return navigator.Select(XPathString, Root.Manager);
        }

        /// <summary>
        /// performs actions appropriate when the item was added
        /// </summary>
        /// <param name="args">
        /// The item created event args
        /// </param>        
        protected virtual void OnItemCreated(OffspringItemEventArgs args)
        {
            if (ItemCreated != null)
            {
                ItemCreated(this, args);    
            }

            // TODO: this should be placed somewhere else
            var choiceItem = ParentItem as ChoiceItem;
            if (choiceItem != null && args.Item.Navigable != null)
            {
                choiceItem.SelectedItemsId = ID;
            }
        }

        /// <summary>
        /// checks if databinding is needed and performs standard on page preload operations
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The event args.
        /// </param>
        protected override void OnPagePreLoad(object sender, EventArgs e)
        {
            base.OnPagePreLoad(sender, e);
            ReindexChildIndexes();

            if (Page != null && !Page.IsPostBack)
            {
                RequiresDataBinding = true;
            }
        }

        /// <summary>
        /// Removes all data that is created when child items are created
        /// </summary>
        protected virtual void ResetChildControls()
        {
            var prototypeItem = items.Where(item => item.IsPrototypeItem).FirstOrDefault();
            ChildControlsCreated = false;
            items.Clear();
            if (prototypeItem != null)
            {
                base.Controls.Add(prototypeItem);
                items.Add(prototypeItem);
            }
        }

        /// <summary>
        /// Checks if the data source is IXPathNavigable
        /// </summary>
        /// <param name="dataSource">
        /// The data source.
        /// </param>
        /// <exception cref="InvalidOperationException">
        /// </exception>
        protected override void ValidateDataSource(object dataSource)
        {
            if (!(dataSource is IXPathNavigable))
            {
                throw new InvalidOperationException("Offsprings data source must implement IXPathNavigable interface");
            }
        }

        /// <summary>
        /// because Visibility is ignored by the designer, the design mode has to be checked
        /// </summary>
        private void AddMarker()
        {
            if (!DesignMode)
            {
                Controls.Add(Marker);
                return;
            }

            if (Marker.Visible)
            {
                Controls.Add(Marker);
            }
        }

        /// <summary>
        /// creates an optional item if there are no items and MinOccurs = 0
        /// or creates a minimal amount of items
        /// </summary>
        /// <param name="count">
        /// the number of already created items
        /// </param>
        private void CreateRemainingItems(int count)
        {
            for (int i = count; i < MinRenderedOccurs; i++)
            {
                AddItem(null);
            }
        }        

        private void ReindexChildIndexes()
        {
            int index = 0;
            foreach (var item in items.Where(i => !i.IsPrototypeItem))
            {
                index++;
                item.Index = index;                
            }

            PostBackHighestIndex = index;
        }

        private void ResetDisplayName()
        {
            DisplayName = null;
        }

        private bool ShouldSerializeDisplayName()
        {
            return DisplayName != Root.DisplayNameMethod(this, EventArgs.Empty);
        }

        private bool ShouldSerializeMinRenderedOccurs()
        {
            return MinRenderedOccurs != DefaultMinRenderedOccurs;
        }

        private bool ShouldSerializeID()
        {
            return this is Root;
        }

        private string GetNameFromId()
        {
            var underscoreIndex = ID.IndexOf("__", StringComparison.Ordinal);
            if (underscoreIndex != -1)
            {
                return string.Concat(ID.Substring(0, underscoreIndex), ":", ID.Substring(underscoreIndex + 2));
            }

            return ID;
        }

        private void UpdateNumberOfItems()
        {
            if (!ChildControlsCreated)
            {
                return;
            }

            var previousItemsCount = HighestIndex;            
            if (previousItemsCount < MinOccurs)
            {
                for (var i = previousItemsCount; i < MinOccurs; i++)
                {
                    AddItem(null);
                }
            }
        }
    }
}